﻿using System;
using System.Linq;
using System.Collections;
using System.Collections.Specialized;
using System.Web;

namespace BoldBrick.Web.Parameters
{
    /// <summary>
    /// Provides functionality for working with query string parameters.
    /// </summary>
    public class RequestQueryParametersProvider
    {
        /// <summary>
        /// Singleton instance of this class.
        /// </summary>
        public static RequestQueryParametersProvider Current
        {
            get { return current; }
        }
        private static readonly RequestQueryParametersProvider current;

        /// <summary>
        /// Extracts query parameters from the current HTTP request.
        /// </summary>
        /// <typeparam name="TParam">Type of query parameters.</typeparam>
        /// <returns>Query parameters object with extracted values from <see cref="HttpContext"/>.</returns>
        public TParam LoadParameters<TParam>()
            where TParam: new()
        {
            if (HttpContext.Current == null)
                throw new NotSupportedException("Not supported in non web context");

            return LoadParameters<TParam>(HttpContext.Current.Request.QueryString);
        }

        /// <summary>
        /// Extracts query parameters from the given Uri object
        /// </summary>
        /// <typeparam name="TParam">Type of query parameters.</typeparam>
        /// <param name="uri">The URI from which to extract parameters.</param>
        /// <returns>Query parameters object with extracted values.</returns>
        public TParam LoadParameters<TParam>(Uri uri)
            where TParam : new()
        {
            if (uri == null)
                throw new ArgumentNullException("uri");

            return LoadParameters<TParam>(uri.Query);
        }

        /// <summary>
        /// Extracts query parameters from the given query string
        /// </summary>
        /// <typeparam name="TParam">Type of query parameters.</typeparam>
        /// <param name="query">Query string.</param>
        /// <returns>Query parameters object with extracted values.</returns>
        public TParam LoadParameters<TParam>(string query)
            where TParam : new()
        {
            if (query == null)
                throw new ArgumentNullException("query");

            NameValueCollection queryColl = HttpUtility.ParseQueryString(query);
            return LoadParameters<TParam>(queryColl);
        }

        /// <summary>
        /// Extracts query parameters from the given <see cref="NameValueCollection"/>.
        /// </summary>
        /// <typeparam name="TParam">Type of query parameters.</typeparam>
        /// <param name="nameValueColl">Collection with query values.</param>
        /// <returns>Query parameters object with extracted values.</returns>
        public TParam LoadParameters<TParam>(NameValueCollection nameValueColl)
            where TParam : new()
        {
            if (nameValueColl == null)
                throw new ArgumentNullException("nameValueColl");

            PageQueryInfo pars = PageQueryParametersCache.GetParametersInfo(typeof(TParam));

            TParam parData = new TParam();
            BindParameterValues(pars, nameValueColl, parData);
            return parData;
        }

        /// <summary>
        /// Builds a query string from the given query parameters data object.
        /// </summary>
        /// <typeparam name="TParam">Type of query parameters.</typeparam>
        /// <param name="queryParams">Query parameters data object.</param>
        /// <returns>New query string.</returns>
        public string BuildQuery<TParam>(TParam queryParams)
            where TParam : new()
        {
            if (queryParams == null)
                throw new ArgumentNullException("queryParams");

            NameValueCollection queryColl = BuildQueryCollection(queryParams);
            return queryColl.ToString();
        }

        /// <summary>
        /// Builds a new <see cref="Uri"/> from an existing <see cref="Uri"/> and a query parameters data object.
        /// </summary>
        /// <typeparam name="TParam">Type of query parameters.</typeparam>
        /// <param name="uri">An existing query to update.</param>
        /// <param name="queryParams">Query parameters data object.</param>
        /// <returns>New <see cref="Uri"/> with updated query string values.</returns>
        public Uri BuildNewUri<TParam>(Uri uri, TParam queryParams)
            where TParam : new()
        {
            if (uri == null)
                throw new ArgumentNullException("uri");
            if (queryParams == null)
                throw new ArgumentNullException("queryParams");

            NameValueCollection queryColl = BuildQueryCollection(queryParams);
            
            UriBuilder uriBldr = new UriBuilder(uri) { Query = queryColl.ToString() };

        	return uriBldr.Uri;
        }

        #region ------ Internals: Conversions -----------------------------------------------------

        private static NameValueCollection BuildQueryCollection<TParam>(TParam queryParams) where TParam : new()
        {
            if (queryParams == null)
                throw new ArgumentNullException("queryParams");

            PageQueryInfo pars = PageQueryParametersCache.GetParametersInfo(typeof(TParam));

            NameValueCollection queryColl = HttpUtility.ParseQueryString(string.Empty);
            BindQuery(pars, queryColl, queryParams);
            return queryColl;
        }

        private static void BindQuery<TParam>(PageQueryInfo pars, NameValueCollection valueColl, TParam queryParams) 
            where TParam : new()
        {
            if (pars == null)
                throw new NullReferenceException("pars");
            if (valueColl == null)
                throw new NullReferenceException("valueColl");
            if (queryParams == null)
                throw new NullReferenceException("queryParams");

            if (pars.Parameters == null)
                return;

            IOrderedEnumerable<QueryParameter> orderedParams = pars.Parameters.OrderBy(p => p.ParamName);
            foreach (QueryParameter p in orderedParams)
            {
                if (p.FieldInfo == null)
                    throw new InvalidOperationException("FieldInfo must be set");

                object val = p.FieldInfo.GetValue(queryParams, null);
                if (val != null)
                    valueColl.Add(p.ParamName, FormatQueryOutputParam(val, p.DataFormatString));
                else
                    if (p.IsRequired)
                        throw new InvalidOperationException("Parameter is required: " + p.ParamName);
            }
        }

        private static string FormatQueryOutputParam(object val, string dataFormatString)
        {
            if (val == null)
                throw new ArgumentNullException("val");

            Type valType = val.GetType();
            if (valType.IsArray)
                return string.Join(",", ((Array)val).Cast<object>().Select(o => FormatQueryOutputValue(o, dataFormatString)).ToArray());

            return FormatQueryOutputValue(val, dataFormatString);
        }

        private static string FormatQueryOutputValue(object val, string dataFormatString)
        {
            Type valType = val.GetType();
            if (valType == typeof(string))
                return (string)val;

            if (valType == typeof(Guid))
                return ((Guid)val).ToString();

            if (valType == typeof(int))
                return ((int)val).ToString();

            if (valType == typeof(bool))
                return ((bool)val).ToString();

            if (valType == typeof(DateTime))
            {
                if (dataFormatString == null)
                    return ((DateTime)val).ToString();
                return ((DateTime)val).ToString(dataFormatString);
            }

            throw new NotSupportedException(valType.Name + " is not supported");
        }

        private static void BindParameterValues<TParam>(PageQueryInfo pars, NameValueCollection valueColl, TParam queryParams) 
            where TParam : new()
        {
            if (pars == null)
                throw new NullReferenceException("pars");
            if (valueColl == null)
                throw new NullReferenceException("valueColl");
            if (queryParams == null)
                throw new NullReferenceException("queryParams");

            if (pars.Parameters == null)
                return;

            foreach (QueryParameter p in pars.Parameters)
            {
                if (p.FieldInfo == null)
                    throw new InvalidOperationException("FieldInfo must be set");

                string val = valueColl[p.ParamName];
                if (string.IsNullOrEmpty(val) && p.IsRequired)
                    throw new InvalidParameterException(p.ParamName);

                try
                {
                    object dest = GetDestinationValue(val, p.FieldInfo.PropertyType, p.DataFormatString);
                    p.FieldInfo.SetValue(queryParams, dest, null);
                }
                catch (FormatException ex)
                {
                    throw new InvalidParameterException(p.ParamName, ex);
                }
            }
        }

        internal static object GetDestinationValue(string val, Type destType, string dataFormatString)
        {
            if (val == null)
                return null;

            if (destType.IsArray)
            {
                Type destElType = destType.GetElementType();
                string[] valArray = val.Split(new char[]{','}, StringSplitOptions.RemoveEmptyEntries);

                ArrayList destList = new ArrayList();
                foreach (string testItem in valArray)
                    destList.Add(
                        ConvertFromString(testItem, destElType, dataFormatString));
                return destList.ToArray(destElType);
            }

            return ConvertFromString(val, destType, dataFormatString);
        }

        private static object ConvertFromString(string val, Type destType, string dataFormatString)
        {
            if (destType == typeof(string))
                return val;

            if (destType == typeof(Guid))
                return new Guid(val);

            if (destType == typeof(int))
                return int.Parse(val);

            if (destType == typeof(bool))
                return bool.Parse(val);

            if (destType == typeof(DateTime))
            {
                if (dataFormatString == null)
                    throw new InvalidOperationException("dataFormatString is required");
                return DateTime.ParseExact(val, dataFormatString, null);
            }

            Type destUnderType = Nullable.GetUnderlyingType(destType);
            if (destUnderType != null)
                return ConvertFromString(val, destUnderType, dataFormatString);

            throw new NotSupportedException(destType.Name + " is not supported to be parsed");
        }

        #endregion

        #region ------ Constructors ---------------------------------------------------------------

        /// <summary>
        /// Static constructor
        /// </summary>
        static RequestQueryParametersProvider()
        {
            // instantiates singleton
            current = new RequestQueryParametersProvider();
        }

        #endregion
    }
}
